/*
  Applicast API Extension/Utility Library
  Wrapper around Google REST geocoder & Static Maps

  (c) 2009 Estanislau Trepat / ILERRED, S.L.

  Applicast API Extension Library is freely
  distributable under the terms of an MIT-style license.
*/

var AppliMaps_Config = {
  API_KEY: '',

  setKey: function(googleMapsApiKey) {
    AppliMaps_Config.API_KEY = googleMapsApiKey;
  },

  isKeySet: function() {
    return AppliMaps_Config.API_KEY !== '';
  }
};

var AppliMaps_GeocoderCache = Class.create({
  initialize: function() {
    this.cache = {};
  },

  get: function(address) {
    var canonical = this.toCanonical(address);
    if ( typeof this.cache[canonical] !== 'undefined' )
      return this.cache[canonical];
    return null;
  },

  put: function(address, response) {
    if ( this.isCacheable(response) ) {
      var canonical = this.toCanonical(address);
      this.cache[canonical] = response;
    }
  },

  isCacheable: function(response) {
    if ( !response )
      return false;
    if ( typeof response.Status !== 'undefined' && typeof response.Status.code !== 'undefined' && String(response.Status.code) === '200' )
      return true;
    return false;
  },

  reset: function() {
    this.cache = null;
    this.cache = {};
  },

  toCanonical: function(address) {
    var canonical = String(address).toLowerCase();
    canonical = canonical.split(',').join(' ');
    canonical = canonical.replace('/\s\s+/g', ' ');
    return canonical;
  }
});

var AppliMaps_GeocoderError = Class.create({
  initialize: function(errno, errmsg) {
    this.errno = errno;
    this.errmsg = errmsg;
  },

  toString: function() {
    return '[' + String(this.errno) + '] ' + this.errmsg;
  }
});

var AppliMaps_Geocoder = Class.create({
  serviceURL: 'http://maps.google.es/maps/geo',

  resultCodes: {
    200: 'No se han producido errores, la dirección se analizó correctamente y se devolvió su código geográfico.',
    400: 'No se ha podido analizar correctamente la solicitud de ruta.',
    500: 'No se ha podido procesar correctamente la solicitud de ruta o de códigos geográficos, sin saberse el motivo exacto del fallo.',
    601: 'Falta el parámetro HTTP q o no tiene valor alguno.',
    602: 'No se ha encontrado una ubicación geográfica que se corresponda con la dirección especificada.',
    603: 'El código geográfico no puede devolverse por motivos jurídicos o contractuales.',
    604: 'No hay ninguna ruta disponible.',
    610: 'La clave proporcionada no es válida o no coincide con el dominio para el cual se ha indicado.',
    620: 'La clave especificada ha superado el límite de solicitudes.'
  },

  initialize: function() {
    this.options = {
      useCache: true,
      onGeocoderSuccess: Prototype.emptyFunction,
      onGeocoderError: Prototype.emptyFunction
    };
    Object.extend(this.options, arguments[0] || { });

    this.cache = new AppliMaps_GeocoderCache();
  },

  reset: function() {
    this.cache.reset();
  },

  get: function(address, options) {
    Object.extend(this.options, options || { });

    if ( this.options.useCache ) {
      var response = this.cache.get(address);
      if ( response ) {
        this.options.onGeocoderSuccess(response);
        return;
      }
    }

    new Ajax.Request(this.serviceURL, {
      //parameters: 'fmt=json&q=' + encodeURIComponent(this.toCanonical(address)),
      parameters: 'output=json&oe=utf8&sensor=false&key=' + AppliMaps_Config.API_KEY + '&q=' + encodeURIComponent(this.toCanonical(address)),
      onSuccess: function(t) {
        try {
          var response = eval('(' + t.responseText + ')');
          var status = this._getStatus(response);
          switch (status) {
            case 200:
              print('[' + status + '] ' + this.resultCodes[status]);
              print('>>> ' + address);
              this.options.onGeocoderSuccess(response);
              if ( this.options.useCache )
                this.cache.put(address, response);
              break;

            default:
              this.options.onGeocoderError(new AppliMaps_GeocoderError(status, this.resultCodes[status]));
              break;
          }
        } catch(e) {
          throw e;
        }
      }.bind(this),
      onFailure: function(t) {
        throw t.responseText;
      },
      onException: function(e) {
        throw e;
      }
    });
  },

  toCanonical: function(address) {
    var canonical = String(address).toLowerCase();
    canonical = canonical.split(',').join(' ');
    canonical = canonical.replace('/\s\s+/g', ' ');
    return canonical;
  },

  _getStatus: function(response) {
    try {
      return response.Status.code || 500;
    } catch(e) {
      return 500;
    }
  }
});

var AppliMaps_Mercator = {

  offset: 268435456,
  radius: 85445659.44705395, // radius = offset / Math.PI

  lonToX: function(lon) {
    return Math.round(AppliMaps_Mercator.offset + AppliMaps_Mercator.radius*lon*Math.PI/180);
  },

  latToY: function(lat) {
    return Math.round(AppliMaps_Mercator.offset-AppliMaps_Mercator.radius*Math.log((1+Math.sin(lat*Math.PI/180))/(1-Math.sin(lat*Math.PI/180)))/2);
  },

  xToLon: function(x) {
    return ((Math.round(x)-AppliMaps_Mercator.offset)/AppliMaps_Mercator.radius)*180/Math.PI;
  },

  yToLat: function(y) {
    return (Math.PI/2-2*Math.atan(Math.exp((Math.round(y)-AppliMaps_Mercator.offset)/AppliMaps_Mercator.radius)))*180/Math.PI;
  },

  adjustLngByPixels: function(lon, delta, zoom) {
    return AppliMaps_Mercator.xToLon(AppliMaps_Mercator.lonToX(lon) + (delta << (21 - zoom)));
  },

  adjustLatByPixels: function(lat, delta, zoom) {
    return AppliMaps_Mercator.yToLat(AppliMaps_Mercator.latToY(lat) + (delta << (21 - zoom)));
  }
};

var AppliMaps_Utils = {
  // Latitude/longitude spherical geodesy formulae & scripts (c) Chris Veness 2002-2009

  /*
  * Use Haversine formula to Calculate distance in meters between two points specified by
  * latitude/longitude objects (AppliMaps_LatLng)
  */
  haversineDistance: function(l1, l2) {
    var R = 6378137;
    var dLat = (l2.lat() - l1.lat()).toRad();
    var dLon = (l2.lng() - l1.lng()).toRad();
    var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
            Math.cos(l1.lat().toRad()) * Math.cos(l2.lat().toRad()) * Math.sin(dLon/2) * Math.sin(dLon/2);
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));

    return R * c;
  }
};

var AppliMaps_LatLng = Class.create({
  initialize: function(lat, lng) {
    this.latitude = lat ? parseFloat(lat) : 0.0;
    this.longitude = lng ? parseFloat(lng) : 0.0;
  },

  lat: function() {
    return this.latitude;
  },

  lng: function() {
    return this.longitude;
  },

  equals: function(other) {
    if ( !other )
      return false;
    if ( typeof other.lat === 'undefined' || typeof other.lng === 'undefined' )
      return false;

    return (this.latitude === other.lat() && this.longitude === other.lng());
  },

  distanceFrom: function(other) {
    if ( !other )
      return 0.0;
    if ( typeof other.lat === 'undefined' || typeof other.lng === 'undefined' )
      return 0.0;

    return AppliMaps_Utils.haversineDistance(this, other);
  },

  toString: function(precision) {
    var decimalPlaces = precision ? precision : 6;
    return this.latitude.toFixed(decimalPlaces) + ',' + this.longitude.toFixed(decimalPlaces);
  },

  toArray: function() {
    return [this.latitude, this.longitude];
  }
});

var AppliMaps_MapMarker = Class.create({
  initialize: function(latlng, size, character, color) {
    this.coordinates = latlng;
    this.size = size ? size : 'mid';
    this.character = character ? character : '';
    this.color = color ? color : 'red';
  },

  lat: function() {
    return this.coordinates.lat();
  },

  lng: function() {
    return this.coordinates.lng();
  },

  toString: function() {
    return this.coordinates.toString() + ',' + this.size + this.color + this.character;
  }
});

var AppliMaps_MapPath = Class.create({
  initialize: function(color, weight) {
    this.type = 'rgba';
    this.color = color ? String(color) : 'ff0000cc';
    this.weight = weight ? String(weight) : '5';
    this.waypoints = [];
  },

  addWayPoint: function(latlng) {
    if ( latlng )
      this.waypoints.push(latlng);
  },

  clear: function() {
    this.waypoints.clear();
  },

  toString: function() {
    waypointStrings = [];
    for(var i=0, m=this.waypoints.length; i < m; ++i)
      waypointStrings.push(this.waypoints[i].toString());
   return this.type + ':0x' + this.color + ',weight:' + this.weight + '|' + waypointStrings.join('|');
  }
});

var AppliMaps_Map = Class.create({
  //serviceURL: 'http://api.braviawidgets.com/mapscast/staticmap/',http://maps.google.com/staticmap?key='.MAPS_API_KEY.'&sensor=false
  serviceURL: 'http://maps.google.com/staticmap',

  initialize: function(node, options) {
    try {
      this.node = $(node);
      this.options = {
        center: null,
        zoom: 14,
        size: {w: 640, h: 480},
        format: 'jpg-baseline',
        maptype: 'mobile',
        markers: [],
        path: null,
        span: null,
        frame: false,
        hl: ''
      };
      Object.extend(this.options, options || { });
    } catch(e) {
      throw e;
    }
  },

  set: function(options) {
    Object.extend(this.options, options || { });
  },

  setCenter: function(latlng) {
    this.set({center: latlng});
    return this;
  },

  getCenter: function() {
    // If we have a center => return it
    if ( this.options.center )
      return this.options.center;

    // If not, but have some markers... calculate it
    if ( this.options.markers.length  >= 1 )
      return this.calculateCenter();

    // Otherwise, invent it ;)
    return new AppliMaps_LatLng(51.477222,0);
  },

  calculateCenter: function() {
    var latSum = lngSum = 0;
    for(var i=0, m=this.options.markers.length; i < m; ++i) {
      latSum += this.options.markers[i].lat();
      lngSum += this.options.markers[i].lng();
    }
    var latAvg = latSum / this.options.markers.length;
    var lngAvg = lngSum / this.options.markers.length;

    return new AppliMaps_LatLng(latAvg, lngAvg);
  },

  setZoomLevel: function(zoomValue) {
    if ( zoomValue >= 1 && zoomValue < 21 )
     this.set({zoom: zoomValue});
    return this;
  },

  resetMarkers: function() {
    this.options.markers.clear();
    return this;
  },

  addMarker: function(marker) {
    if ( marker )
      this.options.markers.push(marker);
    return this;
  },

  zoomIn: function() {
    this.options.zoom++;
    if ( this.options.zoom >= 21 )
      this.options.zoom = 21;
    return this;
  },

  zoomOut: function() {
    this.options.zoom--;
    if ( this.options.zoom < 1 )
      this.options.zoom = 1;
    return this;
  },

  currentZoom: function() {
    return this.options.zoom;
  },

  panNorth: function(pixels) {
    var center = this.getCenter();
    var newLat = AppliMaps_Mercator.adjustLatByPixels(center.lat(), pixels * -1,  this.options.zoom);
    this.setCenter(new AppliMaps_LatLng(newLat, center.lng()));
    return this;
  },

  panSouth: function(pixels) {
    var center = this.getCenter();
    var newLat = AppliMaps_Mercator.adjustLatByPixels(center.lat(), pixels, this.options.zoom);
    this.setCenter(new AppliMaps_LatLng(newLat, center.lng()));
    return this;
  },

  panWest: function(pixels) {
    var center = this.getCenter();
    var newLng = AppliMaps_Mercator.adjustLngByPixels(center.lng(), pixels * -1, this.options.zoom);
    this.setCenter(new AppliMaps_LatLng(center.lat(), newLng));
    return this;
  },

  panEast: function(pixels) {
    var center = this.getCenter();
    var newLng = AppliMaps_Mercator.adjustLngByPixels(center.lng(), pixels, this.options.zoom);
    this.setCenter(new AppliMaps_LatLng(center.lat(), newLng));
    return this;
  },

  setType: function(type) {
    if ( type === 'mobile' || type === 'hybrid' )
      this.set({maptype: type});
    return this;
  },

  updateDisplay: function() {
    this.node.destroyImage();
    this.node.loadImage(this._getMapURI());
    return this;
  },

  _getMapURI: function() {
    var uri = this.serviceURL;
    var params = '?key=' + AppliMaps_Config.API_KEY;
    params += '&sensor=false';
    params += '&size=' + String(this.options.size.w) + 'x' + String(this.options.size.h);
    if ( this.options.center )
      params += '&center=' + this.options.center.toString();
    params += '&zoom=' + String(this.options.zoom);
    params += '&format=' + String(this.options.format);
    params += '&maptype=' + String(this.options.maptype);
    params += '&frame=' + (this.options.frame ? 'true' : 'false' );
    if ( this.options.markers.length >= 1 ) {
      markerStrings = [];
      for(var i=0, m=this.options.markers.length; i < m; ++i)
        markerStrings.push(this.options.markers[i].toString());
      params += '&markers=' + markerStrings.join('|');
    }
    if ( this.options.path )
      params += '&path=' + this.options.path.toString();
    if ( this.options.span )
      params += '&span=' + this.options.span.toString();
    if ( this.options.hl !== '' )
      params += '&hl=' + this.options.hl;



    return String(uri + params);
  }
});